﻿// Copyright 2023 Maintainers of NUKE.
// Distributed under the MIT License.
// https://github.com/nuke-build/nuke/blob/master/LICENSE

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Newtonsoft.Json.Linq;
using Nuke.Common;
using Nuke.Common.IO;
using Nuke.Common.ProjectModel;
using Nuke.Common.Utilities;
using Nuke.Common.Utilities.Collections;
using Scriban;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace Nuke.SourceGenerators;

[Generator]
public class StronglyTypedSolutionGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
    }

    public void Execute(GeneratorExecutionContext context)
    {
        try
        {
            var allTypes = context.Compilation.Assembly.GlobalNamespace.GetAllTypes();
            var members = allTypes.SelectMany(x => x.GetMembers())
                .Where(x => x is IPropertySymbol or IFieldSymbol)
                .Select(x => (Member: x, AttributeData: x.GetAttributeData("global::Nuke.Common.ProjectModel.SolutionAttribute")))
                .Where(x => x.AttributeData?.NamedArguments.SingleOrDefault(x => x.Key == "GenerateProjects").Value.Value as bool? ?? false)
                .ToList();
            if (members.Count == 0)
                return;

            var rootDirectory = GetRootDirectoryFrom(context.Compilation);

            foreach (var (member, attribute) in members)
            {
                var solutionFile = attribute.ConstructorArguments.FirstOrDefault().Value is string { Length: > 0 } relativeSlnPath
                    ? rootDirectory / relativeSlnPath
                    : GetSolutionFileFromParametersFile(rootDirectory, member.Name);
                var fancyNaming = attribute.NamedArguments.SingleOrDefault(x => x.Key == "FancyNames").Value.Value as bool? ?? false;

                var solution = solutionFile.ReadSolution();
                var hintName = member.Name + ".g.cs";
                var declaration = GetDeclaration(solution);

                // lang=csharp
                context.AddSource(hintName, $"""
                    // <auto-generated/>

                    using Microsoft.VisualStudio.SolutionPersistence.Model;
                    using Nuke.Common.ProjectModel;
                    using Nuke.Common.IO;
                    using System.Runtime.CompilerServices;

                    {declaration}
                    """);

                continue;

                [CanBeNull]
                string GetDeclaration(IProjectContainer container)
                {
                    var prefix = new string('_', container.Descendants(x => x.Parent).Count() + 1);
                    var model = new
                    {
                        IsSolution = container is Solution,
                        SolutionReference = container is Solution ? "this" : "solution",
                        Name = GetEscapedName(container switch
                        {
                            Solution => member.Name,
                            SolutionFolder folder => prefix.Substring(1) + folder.Name,
                            _ => throw new ArgumentOutOfRangeException(nameof(container), container, null)
                        }),
                        Projects = container.Projects.OrderBy(x => x.Name).Select(x => new
                        {
                            x.Name,
                            EscapedName = GetEscapedName(x.Name),
                        }),
                        Folders = container.SolutionFolders.OrderBy(x => x.Name).Select(x => new
                        {
                            x.Name,
                            TypeName = prefix + GetEscapedName(x.Name),
                            EscapedName = GetEscapedName(x.Name),
                        }),
                        Declarations = container.SolutionFolders.OrderBy(x => x.Name).Select(GetDeclaration).ToArray(),
                    };

                    // lang=csharp
                    var template = Template.Parse("""
                        {{~ if is_solution ~}}
                        internal class {{ name }}(SolutionModel model, AbsolutePath path) : Nuke.Common.ProjectModel.Solution(model, path)
                        {{~ else ~}}
                        internal class {{ name }}(SolutionFolderModel model, Nuke.Common.ProjectModel.Solution solution) : Nuke.Common.ProjectModel.SolutionFolder(model, solution)
                        {{~ end ~}}
                        {
                        {{~ for project in projects ~}}
                            public Nuke.Common.ProjectModel.Project {{ project.escaped_name }} => this.GetProject("{{ project.name }}");
                        {{~ end ~}}

                        {{~ for folder in folders ~}}
                            public {{ folder.type_name }} {{ folder.escaped_name }} => Unsafe.As<{{ folder.type_name }}>(this.GetSolutionFolder("{{ folder.name }}"));
                        {{~ end ~}}

                        {{~ for declaration in declarations ~}}
                            {{ declaration }}
                        {{~ end ~}}
                        }
                        """);
                    return template.Render(model);

                    string GetEscapedName(string name) => name
                        // .Replace(".", fancyNaming ? "丨" : "_")
                        .Replace(".", fancyNaming ? "٠" : "_")
                        .ReplaceRegex(@"(^[\W^\d]|[\W])", _ => "_");
                }
            }
        }
        catch (Exception exception)
        {
            var diagnostic = Diagnostic.Create(
                "NUKE001",
                nameof(StronglyTypedSolutionGenerator),
                exception.Message,
                DiagnosticSeverity.Error,
                DiagnosticSeverity.Error,
                isEnabledByDefault: true,
                warningLevel: 0);
            context.ReportDiagnostic(diagnostic);
        }

        return;

        static AbsolutePath GetSolutionFileFromParametersFile(AbsolutePath rootDirectory, string memberName)
        {
            var parametersFile = Constants.GetDefaultParametersFile(rootDirectory);
            Assert.FileExists(parametersFile);
            var obj = JObject.Parse(File.ReadAllText(parametersFile));
            var solutionRelativePath = obj[memberName].NotNull($"Property '{memberName}' does not exist in '{parametersFile}'.").Value<string>();
            return rootDirectory / solutionRelativePath.NotNull();
        }

        static AbsolutePath GetRootDirectoryFrom(Compilation compilation)
        {
            var syntaxPath = compilation.SyntaxTrees.First().FilePath;
            var startDirectory = Path.GetDirectoryName(File.Exists(syntaxPath)
                ? syntaxPath
                // For testing only
                : Directory.GetCurrentDirectory());
            return Constants.TryGetRootDirectoryFrom(startDirectory).NotNull();
        }
    }
}
